from __future__ import annotations

from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set
from urllib.parse import urlparse
import logging
import re

if TYPE_CHECKING:
    from cli.core.section import VariableSection

logger = logging.getLogger(__name__)

TRUE_VALUES = {"true", "1", "yes", "on"}
FALSE_VALUES = {"false", "0", "no", "off"}
EMAIL_REGEX = re.compile(r"^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$")


class Variable:
    """Represents a single templating variable with lightweight validation."""

    def __init__(self, data: dict[str, Any]) -> None:
        """Initialize Variable from a dictionary containing variable specification.

        Args:
            data: Dictionary containing variable specification with required 'name' key
                  and optional keys: description, type, options, prompt, value, default, section, origin

        Raises:
            ValueError: If data is not a dict, missing 'name' key, or has invalid default value
        """
        # Validate input
        if not isinstance(data, dict):
            raise ValueError("Variable data must be a dictionary")

        if "name" not in data:
            raise ValueError("Variable data must contain 'name' key")

        # Track which fields were explicitly provided in source data
        self._explicit_fields: Set[str] = set(data.keys())

        # Initialize fields
        self.name: str = data["name"]
        # Reference to parent section (set by VariableCollection)
        self.parent_section: Optional["VariableSection"] = data.get("parent_section")
        self.description: Optional[str] = data.get("description") or data.get(
            "display", ""
        )
        self.type: str = data.get("type", "str")
        self.options: Optional[List[Any]] = data.get("options", [])
        self.prompt: Optional[str] = data.get("prompt")
        if "value" in data:
            self.value: Any = data.get("value")
        elif "default" in data:
            self.value: Any = data.get("default")
        else:
            self.value: Any = None
        self.origin: Optional[str] = data.get("origin")
        self.sensitive: bool = data.get("sensitive", False)
        # Optional extra explanation used by interactive prompts
        self.extra: Optional[str] = data.get("extra")
        # Flag indicating this variable should be auto-generated when empty
        self.autogenerated: bool = data.get("autogenerated", False)
        # Flag indicating this variable is required even when section is disabled
        self.required: bool = data.get("required", False)
        # Flag indicating this variable can be empty/optional
        self.optional: bool = data.get("optional", False)
        # Original value before config override (used for display)
        self.original_value: Optional[Any] = data.get("original_value")
        # Variable dependencies - can be string or list of strings in format "var_name=value"
        # Supports semicolon-separated multiple conditions: "var1=value1;var2=value2,value3"
        needs_value = data.get("needs")
        if needs_value:
            if isinstance(needs_value, str):
                # Split by semicolon to support multiple AND conditions in a single string
                # Example: "traefik_enabled=true;network_mode=bridge,macvlan"
                self.needs: List[str] = [
                    need.strip() for need in needs_value.split(";") if need.strip()
                ]
            elif isinstance(needs_value, list):
                self.needs: List[str] = needs_value
            else:
                raise ValueError(
                    f"Variable '{self.name}' has invalid 'needs' value: must be string or list"
                )
        else:
            self.needs: List[str] = []

        # Validate and convert the default/initial value if present
        if self.value is not None:
            try:
                self.value = self.convert(self.value)
            except ValueError as exc:
                raise ValueError(f"Invalid default for variable '{self.name}': {exc}")

    def convert(self, value: Any) -> Any:
        """Validate and convert a raw value based on the variable type.

        This method performs type conversion but does NOT check if the value
        is required. Use validate_and_convert() for full validation including
        required field checks.
        """
        if value is None:
            return None

        # Treat empty strings as None to avoid storing "" for missing values.
        if isinstance(value, str) and value.strip() == "":
            return None

        # Type conversion mapping for cleaner code
        converters = {
            "bool": self._convert_bool,
            "int": self._convert_int,
            "float": self._convert_float,
            "enum": self._convert_enum,
            "url": self._convert_url,
            "email": self._convert_email,
        }

        converter = converters.get(self.type)
        if converter:
            return converter(value)

        # Default to string conversion
        return str(value)

    def validate_and_convert(self, value: Any, check_required: bool = True) -> Any:
        """Validate and convert a value with comprehensive checks.

        This method combines type conversion with validation logic including
        required field checks. It's the recommended method for user input validation.

        Args:
            value: The raw value to validate and convert
            check_required: If True, raises ValueError for required fields with empty values

        Returns:
            The converted and validated value

        Raises:
            ValueError: If validation fails (invalid format, required field empty, etc.)

        Examples:
            # Basic validation
            var.validate_and_convert("example@email.com")  # Returns validated email

            # Required field validation
            var.validate_and_convert("", check_required=True)  # Raises ValueError if required

            # Autogenerated variables - allow empty values
            var.validate_and_convert("", check_required=False)  # Returns None for autogeneration
        """
        # First, convert the value using standard type conversion
        converted = self.convert(value)

        # Special handling for autogenerated variables
        # Allow empty values as they will be auto-generated later
        if self.autogenerated and (
            converted is None
            or (
                isinstance(converted, str) and (converted == "" or converted == "*auto")
            )
        ):
            return None  # Signal that auto-generation should happen

        # Allow empty values for optional variables
        if self.optional and (
            converted is None or (isinstance(converted, str) and converted == "")
        ):
            return None

        # Check if this is a required field and the value is empty
        if check_required and self.is_required():
            if converted is None or (isinstance(converted, str) and converted == ""):
                raise ValueError("This field is required and cannot be empty")

        return converted

    def _convert_bool(self, value: Any) -> bool:
        """Convert value to boolean."""
        if isinstance(value, bool):
            return value
        if isinstance(value, str):
            lowered = value.strip().lower()
            if lowered in TRUE_VALUES:
                return True
            if lowered in FALSE_VALUES:
                return False
        raise ValueError("value must be a boolean (true/false)")

    def _convert_int(self, value: Any) -> Optional[int]:
        """Convert value to integer."""
        if isinstance(value, int):
            return value
        if isinstance(value, str) and value.strip() == "":
            return None
        try:
            return int(value)
        except (TypeError, ValueError) as exc:
            raise ValueError("value must be an integer") from exc

    def _convert_float(self, value: Any) -> Optional[float]:
        """Convert value to float."""
        if isinstance(value, float):
            return value
        if isinstance(value, str) and value.strip() == "":
            return None
        try:
            return float(value)
        except (TypeError, ValueError) as exc:
            raise ValueError("value must be a float") from exc

    def _convert_enum(self, value: Any) -> Optional[str]:
        if value == "":
            return None
        val = str(value)
        if self.options and val not in self.options:
            raise ValueError(f"value must be one of: {', '.join(self.options)}")
        return val

    def _convert_url(self, value: Any) -> str:
        val = str(value).strip()
        if not val:
            return None
        parsed = urlparse(val)
        if not (parsed.scheme and parsed.netloc):
            raise ValueError("value must be a valid URL (include scheme and host)")
        return val

    def _convert_email(self, value: Any) -> str:
        val = str(value).strip()
        if not val:
            return None
        if not EMAIL_REGEX.fullmatch(val):
            raise ValueError("value must be a valid email address")
        return val

    def to_dict(self) -> Dict[str, Any]:
        """Serialize Variable to a dictionary for storage."""
        result = {}

        # Always include type
        if self.type:
            result["type"] = self.type

        # Include value/default if not None
        if self.value is not None:
            result["default"] = self.value

        # Include string fields if truthy
        for field in ("description", "prompt", "extra", "origin"):
            if value := getattr(self, field):
                result[field] = value

        # Include boolean/list fields if truthy (but empty list is OK for options)
        if self.sensitive:
            result["sensitive"] = True
        if self.autogenerated:
            result["autogenerated"] = True
        if self.required:
            result["required"] = True
        if self.optional:
            result["optional"] = True
        if self.options is not None:  # Allow empty list
            result["options"] = self.options

        # Store dependencies (single value if only one, list otherwise)
        if self.needs:
            result["needs"] = self.needs[0] if len(self.needs) == 1 else self.needs

        return result

    def get_display_value(
        self, mask_sensitive: bool = True, max_length: int = 30, show_none: bool = True
    ) -> str:
        """Get formatted display value with optional masking and truncation.

        Args:
            mask_sensitive: If True, mask sensitive values with asterisks
            max_length: Maximum length before truncation (0 = no limit)
            show_none: If True, display "(none)" for None values instead of empty string

        Returns:
            Formatted string representation of the value
        """
        if self.value is None or self.value == "":
            # Show (*auto) for autogenerated variables instead of (none)
            if self.autogenerated:
                return "[dim](*auto)[/dim]" if show_none else ""
            return "[dim](none)[/dim]" if show_none else ""

        # Mask sensitive values
        if self.sensitive and mask_sensitive:
            return "********"

        # Convert to string
        display = str(self.value)

        # Truncate if needed
        if max_length > 0 and len(display) > max_length:
            return display[: max_length - 3] + "..."

        return display

    def get_normalized_default(self) -> Any:
        """Get normalized default value suitable for prompts and display."""
        try:
            typed = self.convert(self.value)
        except Exception:
            typed = self.value

        # Autogenerated: return display hint
        if self.autogenerated and not typed:
            return "*auto"

        # Type-specific handlers
        if self.type == "enum":
            if not self.options:
                return typed
            return (
                self.options[0]
                if typed is None or str(typed) not in self.options
                else str(typed)
            )

        if self.type == "bool":
            return (
                typed
                if isinstance(typed, bool)
                else (None if typed is None else bool(typed))
            )

        if self.type == "int":
            try:
                return int(typed) if typed not in (None, "") else None
            except Exception:
                return None

        # Default: return string or None
        return None if typed is None else str(typed)

    def get_prompt_text(self) -> str:
        """Get formatted prompt text for interactive input.

        Returns:
            Prompt text with optional type hints and descriptions
        """
        prompt_text = self.prompt or self.description or self.name

        # Add type hint for semantic types if there's a default
        if self.value is not None and self.type in ["email", "url"]:
            prompt_text += f" ({self.type})"

        return prompt_text

    def get_validation_hint(self) -> Optional[str]:
        """Get validation hint for prompts (e.g., enum options).

        Returns:
            Formatted hint string or None if no hint needed
        """
        hints = []

        # Add enum options
        if self.type == "enum" and self.options:
            hints.append(f"Options: {', '.join(self.options)}")

        # Add extra help text
        if self.extra:
            hints.append(self.extra)

        return " — ".join(hints) if hints else None

    def is_required(self) -> bool:
        """Check if this variable requires a value (cannot be empty/None).

        A variable is considered required if:
        - It has an explicit 'required: true' flag (highest precedence)
        - OR it doesn't have a default value (value is None)
          AND it's not marked as autogenerated (which can be empty and generated later)
          AND it's not marked as optional (which can be empty)
          AND it's not a boolean type (booleans default to False if not set)

        Returns:
            True if the variable must have a non-empty value, False otherwise
        """
        # Optional variables can always be empty
        if self.optional:
            return False

        # Explicit required flag takes highest precedence
        if self.required:
            # But autogenerated variables can still be empty (will be generated later)
            if self.autogenerated:
                return False
            return True

        # Autogenerated variables can be empty (will be generated later)
        if self.autogenerated:
            return False

        # Boolean variables always have a value (True or False)
        if self.type == "bool":
            return False

        # Variables with a default value are not required
        if self.value is not None:
            return False

        # No default value and not autogenerated = required
        return True

    def get_parent(self) -> Optional["VariableSection"]:
        """Get the parent VariableSection that contains this variable.

        Returns:
            The parent VariableSection if set, None otherwise
        """
        return self.parent_section

    def clone(self, update: Optional[Dict[str, Any]] = None) -> "Variable":
        """Create a deep copy of the variable with optional field updates.

        This is more efficient than converting to dict and back when copying variables.

        Args:
            update: Optional dictionary of field updates to apply to the clone

        Returns:
            New Variable instance with copied data

        Example:
            var2 = var1.clone(update={'origin': 'template'})
        """
        data = {
            "name": self.name,
            "type": self.type,
            "value": self.value,
            "description": self.description,
            "prompt": self.prompt,
            "options": self.options.copy() if self.options else None,
            "origin": self.origin,
            "sensitive": self.sensitive,
            "extra": self.extra,
            "autogenerated": self.autogenerated,
            "required": self.required,
            "optional": self.optional,
            "original_value": self.original_value,
            "needs": self.needs.copy() if self.needs else None,
            "parent_section": self.parent_section,
        }

        # Apply updates if provided
        if update:
            data.update(update)

        # Create new variable
        cloned = Variable(data)

        # Preserve explicit fields from original, and add any update keys
        cloned._explicit_fields = self._explicit_fields.copy()
        if update:
            cloned._explicit_fields.update(update.keys())

        return cloned
